package 函数式编程.使用Stream;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import ch.qos.logback.core.joran.action.NewRuleAction;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import 其他.system类;


/*

处理包含多个级别的 Stream ，比如 Stream<String[]> 或 Stream<List<LineItem>> 或 Stream<Stream<String>> 
想将 2 级 Stream 扁平化为一级，如 Stream<String> 或 Stream<LineItem>，这样就可以轻松地循环 Stream 并对其进行处理。
Stream<String[]>        -->  Stream<String>
Stream<List<LineItem>>  -->  Stream<LineItem>
Stream<Stream<String>>  -->  Stream<String>
解决方法：
Stream<String[]>      -> flatMap ->	Stream<String>
Stream<Set<String>>   -> flatMap ->	Stream<String>
Stream<List<String>>  -> flatMap ->	Stream<String>
Stream<List<Object>>  -> flatMap ->	Stream<Object>


stream中的flatmap是stream的一种中间操作，它和stream的map一样，是一种收集类型的stream中间操作，但是与map不同的是，它可以对stream流中单
个元素再进行拆分（切片），从另一种角度上说，使用了它，就是使用了双重for循环。

查看Stream源码中flatmap的方法定义：
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

从方法的定义可以看出，其入参是一个函数式接口，该接口的返回类型应该是Stream< ? extends R > 类型的。

从实际需求中查看如何使用flatmap：
需求：有一个补习学校，其中有若干老师教学若干门课程，现在学校有关于数学教学的通知要传达给所有学数学的学生家长，将电子邮件发送到他们的邮箱中。
注意：一个老师可以教学多个科目，一个老师可以教学多个学生，一个学生可以报名多个科目，一个学生可以有多个家长。

数据结构(均省略get/set, toString ,构造器)：
*/
// 老师

@AllArgsConstructor
@Data
class Teacher {
    private String id;
    private String name;
    private String subject;
}
// 学生
@AllArgsConstructor
@Data
class Student {
	private String id;
   private String name;
   private String techId;
    // 重写hashCode及equals方法(id及name相同就为同一个学生)
   public boolean equals(Object o) {
      if (o instanceof Student s) {//Java14中对instanceof进行了升级，不仅仅是直接用来进行类型匹配，还可以直接转换成相应的实例。即：o --> s
         return Objects.equals(this.id,s.id) && Objects.equals(this.name,s.name);
      }
      return false;
   }
   public int hashCode() {
   	return Objects.hash(name,id);
   }
}
// 家长
@Data
class Parents {
    private String id;
    private String name;
    private String chirldId;
    private String email;
}
// 课程
//@Data
enum Subject {
	Math("1", "数学"),Chinese("2", "汉语"),Music("3", "音乐"),English("4", "英语");
    private String value;
    private String desc;
    Subject(String value, String desc) {
        this.value = value;
        this.desc = desc;
    }
    public String getValue() {
   	 return this.value;
    }
}
/*
实际上的处理也比较简单:
1、找出教学科目为“数学”的老师；
2、找到这些老师对应的学生；
3、根据学生找到对应的家长。
 
直接贴代码：*/

public class 使用flatMap {
    // 模拟数据
    public static Student s1 = new Student("1", "zhangsan", "001");
    public static Student s2 = new Student("2", "lisi", "001");
    public static Student s3 = new Student("3", "wangwu", "001");
    public static Student s4 = new Student("4", "zhaoliu", "001");
    public static Student s5 = new Student("6", "tianqi", "001");
    public static Student s6 = new Student("6", "tianqi", "002");
    public static Student s7 = new Student("6", "tianqi", "003");
    public static Teacher t1 = new Teacher("001", "xiaoming", Subject.Math.getValue());
    public static Teacher t2 = new Teacher("002", "lihua", Subject.Music.getValue());
    public static Teacher t3 = new Teacher("003", "hanmeimei", Subject.Math.getValue());
    public static Teacher t4 = new Teacher("004", "lihua", Subject.English.getValue());
    public static List<Student> stus = new ArrayList<>();
    public static List<Teacher> teacs = new ArrayList<>();
    static {
        stus.add(s1);
        stus.add(s2);
        stus.add(s3);
        stus.add(s4);
        stus.add(s5);
        stus.add(s6);
        stus.add(s7);
        teacs.add(t1);
        teacs.add(t2);
        teacs.add(t3);
        teacs.add(t4);
    }
 
 
    public static void main(String[] args) {
        // 找到所有数学老师的学生的家长的电话,并找他们开家长会
        List<Parents> collect = teacs.stream()
                // 过滤数学老师
                .filter(t -> Subject.Math.getValue().equals(t.getSubject()))
                // 通过老师找学生
                .flatMap(t -> stus.stream().filter(s -> s.getTechId().equals(t.getId())))
                // 过滤重复的学生(使用student的equals和hashCode方法)
                .distinct()
                // 通过学生找家长(这里就简化为创建家长对象)
                //UUID 含义是通用唯一识别码 (Universally Unique Identifier)，这是一个软件建构的标准。
                //UUID 的目的，是让分布式系统中的所有元素，都能有唯一的辨识资讯，而不需要透过中央控制端来做辨识资讯的指定。
                //如此一来，每个人都可以建立不与其它人冲突的 UUID。在这样的情况下，就不需考虑数据库建立时的名称重复问题。
                .map(s -> {
                    Parents p = new Parents();
                    p.setId(UUID.randomUUID().toString());
                    p.setChirldId(s.getId());
                    p.setName(s.getName().toUpperCase() + "'s Parent");
                    p.setEmail((int) (Math.random() * 1000000) + "@qq.com");
                    return p;
                }).toList();
        //.collect(Collectors.toList());
        // 打印到控制台看看
        collect.stream().forEach(System.out::println);
        
        //例子一：获得不重复的字母列表
        String[] words = new String[]{"Hello","World"};
        //第一种方式
        List<String[]> a1 = Arrays.stream(words)
                .map(word -> word.split(""))
                .distinct()
                .collect(Collectors.toList());
        a1.forEach(System.out::println);
        //a1.forEach(a -> Arrays.stream(a).forEach(System.out::print));
        
        //第二种方式：flatMap（对流扁平化处理）  Arrays.stream(words) 
        //<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
        List<String> a2 = Arrays.stream(words)
                .map(word -> word.split(""))
                .flatMap(w -> Arrays.stream(w)) //flatMap(Arrays::stream)
                .distinct()
                .collect(Collectors.toList());
        a2.forEach(System.out::println);
        
        //例子二：获得单个大写单词列表
        List<String> fun1 = Arrays.asList("one", "two", "three");
        List<String> fun2 = Arrays.asList("four", "five", "six");
        List<List<String>> nestedList = Arrays.asList(fun1, fun2);
        //方式一：
        nestedList.stream().map(x -> {
            return x.stream().map(a -> a.toUpperCase());
        }).forEach(System.out::println);
        //显然，在最后一步输出的时候，x是一个stream的对象，而不是一个list对象。
        //当我们尝试从List<List<String>>获取值进行操作时，map()无法如预期一样工作，需要进行修改才能从嵌套的List<List<String>>对象获取字符串值。
        
        
        //方式二：
        nestedList.stream().map(x -> {
           return x.stream().map(a -> a.toUpperCase());
        }).forEach(x -> x.forEach(System.out::println));
        
        //方式三：相当于在.flatMap(x -> x.stream())这个时候我们把x.stream()返回的stream对象合并成了一个新的stream对象。
        nestedList.stream().flatMap(x -> x.stream()).map(x->x.toUpperCase()).forEach(System.out::println);

        //flatMap升维
        //给定两个数字列表，如何返回所有的数对呢？例如，给定列表[1, 2, 3]和列表[3, 4]，应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。
        List<Integer> nums1 = Arrays.asList(1, 2, 3);
  		  List<Integer> nums2 = Arrays.asList(3, 4);
  		  List<String> pairs=nums1.stream().flatMap(x -> nums2.stream().map(y -> new int[] {x,y})).map(x->Arrays.toString(x)).toList();
  		  pairs.forEach(System.out::print);
  		  
  		  
  		  //Why flat a Stream?
  		  //处理包含多个级别的 Stream ，比如 Stream<String[]> 或 Stream<List<LineItem>> 或 Stream<Stream<String>> 
  		  //想将 2 级 Stream 扁平化为一级，如 Stream<String> 或 Stream<LineItem>，这样就可以轻松地循环 Stream 并对其进行处理。
  		  //来看个简单的功能实现，以及常犯的一些错误。
  		  //需求： 有 {"a", "b"}, {"c", "d"}, {"e", "f"} 三个数组，要求输出 除去a之后的数据
  		  String[][] array = new String[][]{{"a", "b"}, {"c", "d"}, {"e", "f"}};
  		  // flatMap 将二维数组转换成意味数组, 或者可以说是从 Stream<String[]> 转换成Stream<String>.

  		  String[] result = Stream.of(array)  // Stream<String[]>
  		          .flatMap(Stream::of)        // Stream<String>  flapMap(x -> Stream.of(x))
  		  .toArray(x->new String[x]);    // [a, b, c, d, e, f]  toArray(x-> new String[x]) x是数值的长度
  		  //.toArray(String[]::new);    // [a, b, c, d, e, f]  toArray(x-> new String[x])

  		  Arrays.stream(result).forEach(x -> System.out.printf("\n|---->%s", x));
  		String[] z=new String[] {"2","3"};
    }
}
/*
运行结果:
Parents{id='3d9312eb-0df5-4ec6-998f-94a32c2253b4', name='LISI's Parent', chirldId='2', telNo='844668@qq.com'}
Parents{id='7f0b92f5-872d-4671-982d-ef1b48840ce3', name='WANGWU's Parent', chirldId='3', telNo='563932@qq.com'}
Parents{id='c318bffd-8c6d-4849-8109-9c686c97fb77', name='ZHAOLIU's Parent', chirldId='4', telNo='108022@qq.com'}
Parents{id='a4ff1bbc-c9b6-4ad2-872c-f4df670c7bb6', name='TIANQI's Parent', chirldId='6', telNo='658956@qq.com'}

如果不使用stream，写该部分代码的效果，可能还有优化的空间，但是不如使用stream处理方式更为简洁，方便：
*/
/*
public class Test {
    public static void main(String[] args) {
        List<Parents> pars = new ArrayList<>();
        Set<Student> targetStudents = new HashSet<>();//无重复
        //所有老师循环，过滤出数学老师所教学生
        for (Teacher t : teacs) {
            //过滤出数学老师
            if (t.getSubject().equals(Subject.Math.getValue())) {
                //所有学生循环
                for (Student s : stus) {
                    //过滤出数学老师所教学生
                    if (s.getTechId().equals(t.getId())) {
                        targetStudents.add(s);
                    }
                }
            }
        }
        //数学老师所教学生循环，得出学生家长信息
        for (Student s : targetStudents) {
            Parents p = new Parents();
            p.setId(UUID.randomUUID().toString());
            p.setChirldId(s.getId());
            p.setName(s.getName().toUpperCase() + "'s Parent");
            p.setEmail((int) (Math.random() * 1000000) + "@qq.com");
            pars.add(p);
        }
        for (Parents p : pars) {
            System.out.println(p);
        }
    }
}
*/
/*
再去看stream中的flatmap方法，它的作用就和他的名字flat一样，对于调用flatmap的流的每一个元素，执行flatmap入参中的函数式方法，由于该函数
式方法必须返回一个stream<T>类型的流，这样对于调用flatmap的操作来说，就收集了另一种类型(<T>)的流，并在后续的操作中将<T>类型进行合并，最
终产生一个stream<T>的流，而不是一个stream<stream<T>>类型的流。
*/